import * as React from 'react';
import toArray from 'rc-util/lib/Children/toArray';
import warning from 'rc-util/lib/warning';
import {
  DataNode,
  FlattenNode,
  DataEntity,
  Key,
  GetKey,
  NodeElement,
} from '../interface';
import { getPosition, isTreeNode, parseCheckedKeys } from '../util';
import { conductCheck } from './conductUtil';

export function getKey(key: Key, pos: string) {
  if (key !== null && key !== undefined) {
    return key;
  }
  return pos;
}

/**
 * Warning if TreeNode do not provides key
 */
export function warningWithoutKey(treeData: DataNode[] = []) {
  const keys: Map<string, boolean> = new Map();

  function dig(list: DataNode[], path: string = '') {
    (list || []).forEach((treeNode) => {
      const { key, children } = treeNode;
      warning(
        key !== null && key !== undefined,
        `Tree node must have a certain key: [${path}${key}]`,
      );

      const recordKey = String(key);
      warning(
        !keys.has(recordKey) || key === null || key === undefined,
        `Same 'key' exist in the Tree: ${recordKey}`,
      );
      keys.set(recordKey, true);

      dig(children || [], `${path}${recordKey} > `);
    });
  }

  dig(treeData);
}

/**
 * Convert `children` of Tree into `treeData` structure.
 */
export function convertTreeToData(rootNodes: React.ReactNode): DataNode[] {
  function dig(node: React.ReactNode): DataNode[] {
    const treeNodes = toArray(node) as NodeElement[];
    return treeNodes
      .map((treeNode) => {
        // Filter invalidate node
        if (!isTreeNode(treeNode)) {
          warning(
            !treeNode,
            'Tree/TreeNode can only accept TreeNode as children.',
          );
          return null;
        }

        const { key } = treeNode;
        const { children, ...rest } = treeNode.props;

        const dataNode: DataNode = {
          key: key as any,
          ...rest,
        };

        const parsedChildren = dig(children);
        if (parsedChildren.length) {
          dataNode.children = parsedChildren;
        }

        return dataNode;
      })
      .filter((dataNode) => !!dataNode) as DataNode[];
  }

  return dig(rootNodes);
}

/**
 * generate corresponding expandedKeys according to the given level
 */
export function convertDataToExpandedKeys(
  keyEntities: Record<Key, DataEntity>,
  treeNodeList: DataNode[] = [],
  level: number = 2,
): Key[] {
  const expandedKey: Key[] = [];

  function dig(list: DataNode[]) {
    list.forEach((treeNode) => {
      if (keyEntities[treeNode.key]?.level < level) {
        expandedKey.push(treeNode.key);
      }
      if (treeNode.children) {
        dig(treeNode.children);
      }
    });
  }

  dig(treeNodeList);
  return expandedKey;
}

/**
 * Flat nest tree data into flatten list. This is used for virtual list render.
 * @param treeNodeList Origin data node list
 * @param expandedKeys
 * need expanded keys, provides `true` means all expanded (used in `rc-tree-select`).
 */
export function flattenTreeData(
  treeNodeList: DataNode[] = [],
  expandedKeys: Key[] | true = [],
): FlattenNode[] {
  const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
  // const flattenList: FlattenNode[] = [];

  function dig(
    list: DataNode[],
    parent: FlattenNode = null as any,
  ): FlattenNode[] {
    return list.map((treeNode, index) => {
      const pos: string = getPosition(parent ? parent.pos : '0', index);
      const mergedKey = getKey(treeNode.key, pos);

      // Add FlattenDataNode into list
      const flattenNode: FlattenNode = {
        ...treeNode,
        parent,
        pos,

        children: null as any,
        data: treeNode,
        isStart: [...(parent ? parent.isStart : []), index === 0],
        isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
      };

      // flattenList.push(flattenNode);

      // Loop treeNode children
      if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
        flattenNode.children = dig(treeNode.children || [], flattenNode);
      } else {
        flattenNode.children = [];
      }

      return flattenNode;
    });
  }

  // dig(treeNodeList);

  return dig(treeNodeList);
}

type ExternalGetKey = GetKey<DataNode> | string;

interface TraverseDataNodesConfig {
  childrenPropName?: string;
  externalGetKey?: ExternalGetKey;
}

/**
 * Traverse all the data by `treeData`.
 * Please not use it out of the `rc-tree` since we may refactor this code.
 */
export function traverseDataNodes(
  dataNodes: DataNode[],
  callback: (data: {
    node: DataNode;
    index: number;
    pos: string;
    key: Key;
    parentPos: string | number;
    level: number;
  }) => void,
  // To avoid too many params, let use config instead of origin param
  config?: TraverseDataNodesConfig | ExternalGetKey,
) {
  // Init config
  let externalGetKey: ExternalGetKey = null as any;
  let childrenPropName: string = '';

  const configType = typeof config;

  if (configType === 'function' || configType === 'string') {
    // Legacy getKey param
    externalGetKey = config as ExternalGetKey;
  } else if (config && configType === 'object') {
    ({ childrenPropName, externalGetKey } = config as any);
  }

  childrenPropName = childrenPropName || 'children';

  // Get keys
  let syntheticGetKey: (node: DataNode, pos?: string) => Key;
  if (externalGetKey) {
    if (typeof externalGetKey === 'string') {
      syntheticGetKey = (node: DataNode) =>
        (node as any)[externalGetKey as string];
    } else if (typeof externalGetKey === 'function') {
      syntheticGetKey = (node: DataNode) =>
        (externalGetKey as GetKey<DataNode>)(node);
    }
  } else {
    syntheticGetKey = (node, pos) => getKey(node.key, pos as any);
  }

  // Process
  function processNode(
    node: DataNode,
    index?: number,
    parent?: { node: DataNode; pos: string; level: number },
  ) {
    const children = node ? (<any>node)[childrenPropName] : dataNodes;
    const pos = node ? getPosition(parent?.pos as any, index as any) : '0';

    // Process node if is not root
    if (node) {
      const key: Key = syntheticGetKey(node, pos);
      const data = {
        node,
        index,
        pos,
        key,
        parentPos: parent?.node ? parent.pos : null,
        level: (parent?.level || 0) + 1,
      };

      callback(data as any);
    }

    // Process children node
    if (children) {
      children.forEach((subNode: any, subIndex: any) => {
        processNode(subNode, subIndex, {
          node,
          pos,
          level: parent ? parent.level + 1 : -1,
        });
      });
    }
  }

  processNode(null as any);
}

interface Wrapper {
  posEntities: Record<string, DataEntity>;
  keyEntities: Record<Key, DataEntity>;
}

export function convertDataToTreeData(
  dataNodes: DataNode[],
  oCheckedKeys: Key[] = [],
  checkable?: boolean,
): DataNode[] {
  if (checkable) {
    return dataNodes;
  }

  const entitiesMap = convertDataToEntities(dataNodes);
  const checkedKeyEntity = parseCheckedKeys(oCheckedKeys);
  let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity;

  const conductKeys = conductCheck(checkedKeys, true, entitiesMap.keyEntities);
  ({ checkedKeys, halfCheckedKeys } = conductKeys);

  checkedKeys = [...checkedKeys, ...halfCheckedKeys];

  function dig(list: DataNode[]): DataNode[] {
    return list
      .map((treeNode) => {
        if (checkedKeys.includes(treeNode.key)) {
          if (treeNode.children) {
            treeNode.children = dig(treeNode.children);
            if (!treeNode.children?.length) {
              treeNode.children = undefined;
            }
          }
          return treeNode;
        }
        return null as any;
      })
      .filter((treeNode) => !!treeNode);
  }
  return dig(dataNodes);
}

/**
 * Convert `treeData` into entity records.
 */
export function convertDataToEntities(
  dataNodes: DataNode[],
  {
    initWrapper,
    processEntity,
    onProcessFinished,
    externalGetKey,
    childrenPropName,
  }: {
    initWrapper?: (wrapper: Wrapper) => Wrapper;
    processEntity?: (entity: DataEntity, wrapper: Wrapper) => void;
    onProcessFinished?: (wrapper: Wrapper) => void;
    externalGetKey?: ExternalGetKey;
    childrenPropName?: string;
  } = {},
  /** @deprecated Use `config.externalGetKey` instead */
  legacyExternalGetKey?: ExternalGetKey,
) {
  // Init config
  const mergedExternalGetKey = externalGetKey || legacyExternalGetKey;

  const posEntities: Record<string, DataEntity> = {};
  const keyEntities: Record<Key, DataEntity> = {};
  let wrapper = {
    posEntities,
    keyEntities,
  };

  if (initWrapper) {
    wrapper = initWrapper(wrapper) || wrapper;
  }

  traverseDataNodes(
    dataNodes,
    (item) => {
      const { node, index, pos, key, parentPos, level } = item;
      const entity: DataEntity = { node, index, key, pos, level };

      const mergedKey = getKey(key, pos);

      posEntities[pos] = entity;
      keyEntities[mergedKey] = entity;

      // Fill children
      entity.parent = posEntities[parentPos];
      if (entity.parent) {
        entity.parent.children = entity.parent.children || [];
        entity.parent.children.push(entity);
      }

      if (processEntity) {
        processEntity(entity, wrapper);
      }
    },
    { externalGetKey: mergedExternalGetKey, childrenPropName },
  );

  if (onProcessFinished) {
    onProcessFinished(wrapper);
  }

  return wrapper;
}

export interface TreeNodeRequiredProps {
  expandedKeys?: Key[];
  checkedKeys: Key[];
  halfCheckedKeys: Key[];
  keyEntities: Record<Key, DataEntity>;
}
